import curses
import msvcrt
import time
from collections import namedtuple
from random import choice


# 初始化屏幕
STDSRC = curses.initscr()
# 隐藏光标
curses.curs_set(0)

# 定义按键映射
DIRECTIONS = ['Up', 'Left', 'Right', 'Down']
ACTIONS = DIRECTIONS + ['Restart', 'Quit']
LETTER_CODES = [ord(ch) for ch in 'WADSRQwadsrq']
ACTION_MAP = dict(zip(LETTER_CODES, ACTIONS * 2))

# 定义方向向量
VECTOR = dict(zip(DIRECTIONS, [(-1, 0), (0, -1), (0, 1), (1, 0)]))

# 定义非法移动，禁止掉头
NO_TURNS = dict(zip(DIRECTIONS, DIRECTIONS[::-1]))

# 定义坐标类
Coord = namedtuple('Coord', ['row', 'col'])


# 坐标相加
def coord_add(c1, c2):
    c1_row, c1_col = c1
    c2_row, c2_col = c2
    return Coord(c1_row + c2_row, c1_col + c2_col)


# 向屏幕指定位置添加字符串
def cast(s, row=0, col=0):
    STDSRC.addstr(row, col, str(s))
    STDSRC.refresh()


# 游戏类
class Snake(object):
    def __init__(self, width=80, height=30, head_char='@', body_char='o', food_char='$'):
        self.rows = height
        self.cols = width
        self.head_char = head_char
        self.body_char = body_char
        self.food_char = food_char
        self._snake = []
        self._food = Coord(-1, -1)        
        self._score = 0
        self._high_score = 0
        self._curr_direction = ''

        self.reset()

    # 初始化蛇
    def _init_snake(self):
        self._snake = [Coord(self.rows // 2, self.cols // 4 + c)
                       for c in range(3)]

    # 获取空余空间
    def _get_free_spaces(self):
        return [Coord(r, c)
                for r in range(1, self.rows - 1)
                for c in range(1, self.cols - 1)
                if Coord(r, c) not in self._snake]

    # 生成一个食物
    def _create_food(self):
        self._food = choice(self._get_free_spaces())

    # 判断是否吃到自身
    def _is_eat_self(self):
        return self._snake[-1] in self._snake[:-1]

    # 判断是否撞墙
    def _is_hit_wall(self):
        return self._snake[-1].row not in range(1, self.rows - 1) \
            or self._snake[-1].col not in range(1, self.cols - 1)

    # 判断游戏是否结束
    def is_game_over(self):
        return self._is_eat_self() or self._is_hit_wall() or not self._get_free_spaces()

    # 判断是否吃到食物
    def _is_eat_food(self):
        return self._snake[-1] == self._food

    # 吃食物
    def _eat_food(self):
        self._score += 10
        self._create_food()

    # 判断是否掉头
    def _is_turn(self, direction):
        return direction == NO_TURNS[self._curr_direction]

    # 移动操作
    def move(self, direction):
        if self._is_turn(direction):
            direction = self._curr_direction

        self._curr_direction = direction
        head = coord_add(self._snake[-1], VECTOR[direction])
        self._snake.append(head)
        self._draw_head()  # 画蛇头

        if self._is_eat_food():
            self._eat_food()
        else:
            self._erase_tail()  # 擦蛇尾
            self._snake.pop(0)

    # 初始化绘图
    def _init_draw(self):
        def draw_border():
            cast('*' * self.cols)
            cast('*' * self.cols, self.rows - 1)

            for r in range(self.rows):
                cast('*', r, 0)
                cast('*', r, self.cols - 1)

        def draw_snake():
            cast(self.head_char, *self._snake[-1])
            
            for body in self._snake[:-1]:
                cast(self.body_char, *body)

        draw_border()
        draw_snake()

    # 绘制蛇头
    def _draw_head(self):
        cast(self.head_char, *self._snake[-1])
        cast(self.body_char, *self._snake[-2])

    # 擦除蛇尾
    def _erase_tail(self):
        cast(' ', *self._snake[0])

    # 绘制食物
    def draw_food(self):
        cast(self.food_char, *self._food)

    # 绘制 UI
    def draw_ui(self):
        cast("SCORE: {self._score}    HIGH SCORE: {self._high_score}"
             .format(self=self)
             .center(self.cols, '*'))

    # 绘制游戏结束界面
    def draw_game_over(self):
        cast('GAME OVER!'.center(self.cols), self.rows // 2)
        cast('Press <R/r> to Restart, <Q/q> to Quit!'.center(self.cols), self.rows // 2 + 1)

    # 重置游戏
    def reset(self, clear_best=False):
        if clear_best:
            self._high_score = 0
        elif self._score > self._high_score:
            self._high_score = self._score

        self._score = 0
        self._curr_direction = 'Right'
        self._init_snake()
        self._create_food()
        self._init_draw()


def main():
    key = ord('d')  # 初始方向按键，向右 <Right><d>
    old_key = key

    # 获取按键
    def get_key():
        nonlocal key
        nonlocal old_key
        
        if msvcrt.kbhit():  # 判断是否有按键敲击，实现无阻塞输入（无需 <Enter> 确认）
            key = ord(msvcrt.getch())
            
            if key not in ACTION_MAP:
                key = old_key
            else:
                old_key = key

    # Game Over 界面等待 <重新开始> 或 <退出>
    def try_game_over():
        while True:
            get_key()

            if ACTION_MAP[key] == 'Restart':
                return
            if ACTION_MAP[key] == 'Quit':
                quit()

    # 协调速度，由于控制台字符 1x列高 ≈ 1.5x行宽造成的视觉差
    def sleep(sec=0.1):
        if ACTION_MAP[key] in ['Up', 'Down']:
            time.sleep(sec)
        else:
            time.sleep(sec / 1.5)

    snake = Snake()

    while True:
        get_key()

        if ACTION_MAP[key] == 'Restart':
            STDSRC.clear()
            snake.reset()
            key = ord('d')
            old_key = key
        elif ACTION_MAP[key] == 'Quit':
            quit()
        else:
            snake.move(ACTION_MAP[key])
            snake.draw_food()
            snake.draw_ui()

            if snake.is_game_over():
                snake.draw_game_over()
                try_game_over()

        sleep()


if __name__ == '__main__':
    main()
